iOS初级教程之三:Crash分析实践
[iOS初级教程之三]Crash分析实践
一、引言
Crash分析与治理是移动端开发人员的必备技能,Crash相关数据也是衡量应用程序质量的重要指标。本篇文章,我们将讨论在iOS开发中基础的Crash治理实践经验,帮助初学者快速的掌握Crash治理技能,提升工作能力。文章将从如下几个方面进行介绍:
- Crash的统计和分析
- 如何通过友盟APM平台做监控和报警
- SDK收集工具的集成
各种类型的Crash分析实践
Crash治理的重要一步是对Crash进行统计和分析,有了Crash的统计数据,我们才能具体的对某些Crash问题进行分析和处理,友盟U-APM平台提供了非常好的辅助工具,开发者的接入非常简单容易,其可以帮助开发者快速发现问题,统计问题,分析问题最终解决问题。
二、U-APM SDK的集成
客户端应用集成U-APM SDK主要用来进行崩溃检测,卡顿检测以及场景记录等功能。如果使用CocoaPods工具其接入非常简单,在Podfile文件中添加如下依赖即可:
1 | pod 'UMCommon' |
其中UMCommon是友盟SDK基础的支持库,提供SDK初始化等功能,UMDevice库与设备信息功能相关,UMAPM用来做性能与崩溃统计,UMCCommonLog是一个调试库,在开发时我们可以将其引用,用来查看上报情况。
如果项目没有使用CocoaPods,也可以采用手动引入的方式来集成SDK。在如下地址可以根据需求下载到指定的SDK资源:
https://developer.umeng.com/sdk/android?spm=a213m0.21038855.9168240680.3.6a311904uispVD
手动集成SDK还需要做一些简单的工程配置:
1.需要依赖如下系统库:
1 | CoreTelephony.framework |
2.在工程的Targets->BuildSettings 中 , Other Linker Flags增加-ObjC参数。
完成了上面的配置过程,需要编写代码来完成U-APM SDK的接入工作,示例代码如下:
1 |
|
由于需要选择一个较早的实际来进行SDK的初始化,因此我们通常会将初始化的相关代码放入didFinishLaunching方法中,也可以根据具体需求选择初始化的时机,接入SDK基本分为了4个步骤,上面示例代码中有详细的注释,在第1步初始化SDK中,传入的AppKey的值是在友盟后台创建应用后得到的。第2步调用的setCrashCBBlock用来设置上报Crash时的额外信息,通常在这个回调中我们可以将当前登录的用户信息等进行上报。
我们可以手动写一些常见的会产生Crash的代码,在真机上运行上面示例代码后,可以在友盟的APM后台看到记录的异常信息,在上报的日志的自定义字段中,可以看到我们设置的额外上报数据,如下图所示:
此时,你会发现我们收集到的很多堆栈信息都是未符号化的,即都是内存地址,并没与类与方法的信息,这是因为我们还需要配置下应用的符号表,使用Xcode在构建工程时,默认只会在生产环境生成符号表,我们也可以将Build Settings->Debug Information Format选项设置为DWARF with DSYM File来使其在Debug环境下也生成符号表,如下:
编译后生成的符号表会与App包放在同一文件下,我们需要在友盟U-APM后台的设置页面将此符号表文件进行上传,之后就可以正常的对堆栈信息进行解析。如下图:
三、分析用户路径与监控告警
有时候我们记录到了线上的Crash,并且定位到了具体的页面,但是依然无法复现出相同的问题。很多情况下这是因为我们的复现路径与用户的操作路径并不一样,在友盟APM后台,对于收集到了异常问题,除了有详细的堆栈日志和自定义的上报数据外,还可以获取到用户的页面操作路径和设备信息,页面操作路径是非常重要的分析数据,根据这个路径我们可以大致还原出用户打开应用程序后的操作路径,方便我们对问题进行分析复现。如下图所示:
在设备信息页面可以对设备与操作系统相关信息进行查看,如下图:
U-APM后台还提供了非常强大的监控与告警功能,我们可以设置一定的阈值作为报警条件。当某一刻异常问题触发了我们的报警规则,我们可以及时的收到反馈并及时的做出响应。在U-APM后台的检测报警功能页面,我们可以创建一种告警计划,如下图所示:
在创建告警计划时,可以设置一些触发条件,例如在最近一小时内触发的错误数超过阈值,则进行告警。对于告警的方式,有钉钉机器人提醒,邮件,企业微信等,可以参照文档根据需要进行配置。
四、常见Crash分析实践
1.未实现的选择器
未实现的选择器应该是开发中最常见的Crash原因之一,初学者在编写代码时,经常会在控制台看到如下类型的错误提示:
1 | unrecognized selector sent to instance |
这通常是因为调用了没有实现的方法或者执行方法的对象类型不对,我们将这类问题统称为未实现的选择器问题。产生这类的问题的场景通常有如下几种:
①.声明方法未实现
例如在.h文件中声明了一个方法,并在其他地方对此方法进行了调用,但是此方法并没有在.m文件中实现,此时编译工程是不会有问题的,在运行时如果调用到了此未实现的方法会产生崩溃。
②.协议方法未实现
这种场景与声明方法未实现类似,有时候,协议中定义的方法并不一定都是必须实现的,为了避免出现此类问题,我们可以在调用协议方法之前先进行安全判断,如下:
1 | if ([self.delegate respondsToSelector:@selector(protocolMehtod)]) { |
③.copy修饰了可变属性
在定义属性时,如果我们将一个可变的属性使用了copy进行修饰,则在赋值时会隐式的将其拷贝成不可变的类型,这时如果我们调用了可变属性的方法就会产生异常,例如:
1 | @property (nonatomic, copy) NSMutableArray *mutableArray; |
这种场景具有很好的隐秘性,无论是赋值还是方法的调用,Xcode的自动检查功能都不能提前将问题指出,也不会有警告产生。
④.动态调用了未知方法
Objective-C本身是一种动态的语言,有很多种方式可以动态的进行方法的调用,这类调用是不会做编译时检查的,方法名写错或对象类型不对都会产生异常,因此最好在动态方法调用前,都进行安全判断,例如:
1 | if ([self respondsToSelector:@selector(unknow)]) { |
⑤.低版本使用了高版本的API
当低版本系统使用了高版本才有的接口时,也会产生未实现的选择器异常,对于这种场景,Xcode会有警告提示,我们可以在调用方法前,先进行版本的判断,示例如下:
1 | // iOS 13 之后API |
2.KVC相关异常
KVC(Key Value Coding)又称键值编码,其指在iOS开发中,可以允许开发者通过key名直接访问对象的属性或者给对象的属性赋值,而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。很多高级的iOS开发技巧都是基于KVC实现的。
KVC的几个核心方法列举如下:
1 | //核心方法: |
KVC相关的Crash场景主要有两种:
①. 所使用了值为nil的key
当我们使用KVC的方式向对象的属性进行赋值时,要保证Key值不为nil,否则会产生异常,在使用时要做下Key值的判空,如下:
1 | NSString *key = nil; |
②. 使用了对象中不存在的key值
在调用setValue:forKey:方法时,即是传入的Key值不为nil,也有可能会产生异常,默认情况下,如果要操作的属性对象中并不存在,则也会产生Crash,我们可以实现KVC中的如下两个方法来做兼容:
1 | // 为不存在的属性进行KVC赋值时会调用这个方法 |
3.野指针相关异常
由于野指针问题产生的相关异常通常是比较难处理和定位的。野指针通常指所指向的对象已经被释放的指针,其所指向的内存地址存储的数据也被称为僵尸对象。我们可以通过开启Xcode的僵尸对象功能来在开发阶段提前进行预防。在Xcode的scheme编辑中,将Zombie Objects进行勾选即可。如下:
野指针相关问题的异常场景主要有如下几种:
①. 使用了未初始化的对象
②. ARC下,使用了assign或unsafe_unretained修饰对象
如下:
1 | @property (nonatomic, assign) UIView *subView; |
这种场景下,对象释放后,ARC不会自动的帮我们做指针置空操作。
③.runtime关联对象使用了不合适的修饰,如OBJC_ASSOCIATION_ASSIGN
原因与场景2类似,对于对象属性的修饰要使用正确的修饰符。
4.KVO相关异常
KVO全称Key Value Observing,是Apple提供的一套事件通知机制。其允许一个对象监听另外一个对象特定属性的变化,由于KVO的实现机制的原因,一般继承自NSObject的对象才能使用,并且其只对属性才会发生作用。
KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听,KVO可以监听单个属性的变化,也可以监听集合对象的变化。
在某些场景下如果不恰当的使用KVO,也会产生Crash,常见场景如下:
①.被观察者是局部变量
②.观察者是局部变量
③.未实现监听方法
④.重复移除监听对象
要避免上述问题,在使用KVO时要把握两个核心重点:
1. 注意监听对象与被监听对象的生命周期
2. addObserver和removeObserver要成对出现
5.集合对象操作相关Crash
这类Crash主要指不当的操作数组或字典所产生的的。
①.数组越界问题
②.向数组中添加nil元素
③.遍历数组过程中使用了错误的方式修改了数组
④. 字典设置nil值
6.多线程操作相关Crash
和野指针问题类似,多线程产生的异常往往也是比较难定位和解决的。这类异常通常并不好复现,我们在编写代码时要将尽量将逻辑梳理清楚。常见问题场景如下:
①. group enter 与 group leave
在使用GCD多多线程开发时,dispatch\_group\_t是很常用的一种进行任务依赖编程的方式,需要注意,在使用dispatch\_group\_t时,要确保group enter 与 group leave是成对调用的,否则极易出现死锁问题。
②.子线程做UI操作
在子线程中操作UI不仅会造成页面更新不及时,页面混乱等问题,也极易产生异常从而Crash,因此在做UI操作时,一定要保证是在主线程执行。
③.多线程对对象进行释放
在多个线程中对变量进行赋值操作会造成,会造成变量所引用的旧的对象的多线程释放问题,会出现偶现crash。因此,如果有多线程对外部变量进行赋值的操作,我们可以使用信号量进行加锁,保证变量的赋值是串行的,示例代码如下:
1 | __block NSObject *obj; |
④.多线程同时操作数组
多线程同时对数组进行操作也是比较危险的行为,例如当一个线程在对数据进行遍历时,另一个线程改变了数组元素的个数,会由于索引错乱而产生意想不到的问题甚至Crash。在多线程中遍历数组时,可以将数组拷贝一份在进行操作。
7.watch dog异常
为了防止一个应用占用过多的系统资源,Apple设计了一个名为“看门狗”( watchdog )的机制。在不同的场景下,“看门狗”会监测应用的性能。如果超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程。开发者们在 crashlog 里面,会看到诸如 0x8badf00d 这样的错误代码。异常代码:“0x8badf00d ”(看上去非常像 bad food)。
Watch Dog的Crash本身并不是代码错误,其是一种保护机制,当我们收集到的异常有发现这类问题时,就要着重考虑下应用的性能,同时检查是否会有死锁等异常逻辑的产生。
五、建议
1. 重视每一个Crash处理
2. 有监控,紧急问题可以及时响应
3. 积累治理经验
4. 代码规范,安全
5. 逻辑设计尽量简单,多线程场景要清晰